# Copyright (c) Quectel Wireless Solution, Co., Ltd.All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import utime
import osTimer
import quecIot

from queue import Queue

from usr.logging import getLogger
from usr.settings import settings
from usr.settings import PROJECT_NAME
from usr.settings import PROJECT_VERSION
from usr.ota import SotaDownloadUpgrade

log = getLogger(__name__)

object_model = [
    # property
    (9,  ('power_switch', 'rw')),
    (4,  ('energy', 'r')),
    (23, ('phone_num', 'rw')),
    (24, ('loc_method', 'rw')),
    (25, ('work_mode', 'rw')),
    (26, ('work_cycle_period', 'rw')),
    (19, ('local_time', 'r')),
    (15, ('low_power_alert_threshold', 'rw')),
    (16, ('low_power_shutdown_threshold', 'rw')),
    (12, ('sw_ota', 'rw')),
    (13, ('sw_ota_auto_upgrade', 'rw')),
    (10, ('sw_voice_listen', 'rw')),
    (11, ('sw_voice_record', 'rw')),
    (27, ('sw_fault_alert', 'rw')),
    (28, ('sw_low_power_alert', 'rw')),
    (29, ('sw_over_speed_alert', 'rw')),
    (30, ('sw_sim_abnormal_alert', 'rw')),
    (31, ('sw_disassemble_alert', 'rw')),
    (32, ('sw_drive_behavior_alert', 'rw')),
    (21, ('drive_behavior_code', 'r')),
    (33, ('power_restart', 'w')),
    (34, ('over_speed_threshold', 'rw')),
    (36, ('device_module_status', 'r')),
    (37, ('gps_mode', 'r')),
    (38, ('user_ota_action', 'w')),
    (41, ('voltage', 'r')),
    (42, ('ota_status', 'r')),
    (43, ('current_speed', 'r')),

    # event
    (6,  ('sos_alert', 'r')),
    (14, ('fault_alert', 'r')),
    (17, ('low_power_alert', 'r')),
    (18, ('sim_abnormal_alert', 'r')),
    (20, ('disassemble_alert', 'r')),
    (22, ('drive_behavior_alert', 'r')),
    (35, ('over_speed_alert', 'r')),
]

object_model_struct = {
    'device_module_status': {
        'net': 1,
        'location': 2,
        'temp_sensor': 3,
        'light_sensor': 4,
        'move_sensor': 5,
        'mike': 6,
    },
    'loc_method': {
        'gps': 1,
        'cell': 2,
        'wifi': 3,
    },
    'ota_status': {
        'sys_current_version': 1,
        'sys_target_version': 2,
        'app_current_version': 3,
        'app_target_version': 4,
        'upgrade_module': 5,
        'upgrade_status': 6,
    },
}

object_model_code = {i[1][0]: i[0] for i in object_model}


class QuecThing(object):
    def __init__(self, pk, ps, dk, ds, server, downlink_queue):
        self.pk = pk
        self.ps = ps
        self.dk = dk
        self.ds = ds
        self.server = server
        self.file_size = 0
        self.need_download_size = 0
        self.crc_value = 0
        self.download_size = 0
        self.start_addr = 0
        self.downlink_queue = downlink_queue
        self.post_result_wait_queue = Queue(maxsize=16)
        self.quec_timer = osTimer()
        self.cloud_init()

    def cloud_init(self, enforce=False):
        current_settings = settings.get()
        log.debug(
            '[cloud_init start] enforce: %s QuecThing Work State: %s, quecIot.getConnmode(): %s'
            % (enforce, quecIot.getWorkState(), quecIot.getConnmode())
        )
        if enforce is False:
            if quecIot.getWorkState() == 8 and quecIot.getConnmode() == 1:
                return True

        quecIot.init()
        quecIot.setEventCB(self.eventCB)
        quecIot.setProductinfo(self.pk, self.ps)
        if self.dk or self.ds:
            quecIot.setDkDs(self.dk, self.ds)
        quecIot.setServer(1, self.server)
        quecIot.setLifetime(current_settings['sys']['cloud_life_time'])
        quecIot.setMcuVersion(PROJECT_NAME, PROJECT_VERSION)
        quecIot.setConnmode(1)

        count = 0
        while quecIot.getWorkState() != 8 and count < 10:
            utime.sleep_ms(200)
            count += 1

        if not self.ds and self.dk:
            count = 0
            while count < 3:
                dkds = quecIot.getDkDs()
                if dkds:
                    self.dk, self.ds = dkds
                    break
                count += 1
                utime.sleep(count)
            cloud_init_params = current_settings['sys']['cloud_init_params']
            cloud_init_params['DS'] = self.ds
            settings.set('cloud_init_params', cloud_init_params)
            settings.save()

        log.debug('[cloud_init over] QuecThing Work State: %s, quecIot.getConnmode(): %s' % (quecIot.getWorkState(), quecIot.getConnmode()))
        if quecIot.getWorkState() == 8 and quecIot.getConnmode() == 1:
            return True
        else:
            return False

    def cloud_close(self):
        quecIot.setConnmode(0)

    def get_post_res(self):
        self.quec_timer.start(5000, 0, self.quec_timer_cb)
        res = self.post_result_wait_queue.get()
        self.quec_timer.stop()
        return res

    def quec_timer_cb(self, args):
        self.post_result_wait_queue.put(False)
        self.quec_timer.stop()

    @staticmethod
    def rm_empty_data(data):
        for k, v in data.items():
            if not v:
                del data[k]

    def post_data(self, data):
        res = True
        # log.debug('post_data: %s' % str(data))
        for k, v in data.items():
            if object_model_code.get(k) is not None:
                # Event Data Format From object_mode_code
                if v is not None:
                    if isinstance(v, dict):
                        nv = {}
                        for ik, iv in v.items():
                            if object_model_code.get(ik):
                                nv[object_model_code.get(ik)] = iv
                            else:
                                if object_model_struct.get(k):
                                    nv[object_model_struct[k].get(ik)] = iv
                                else:
                                    nv[ik] = iv
                        v = nv
                    # log.debug('k: %s, v: %s' % (k, v))
                    phymodelReport_res = quecIot.phymodelReport(1, {object_model_code.get(k): v})
                    if not phymodelReport_res:
                        res = False
                        break
                else:
                    continue
            elif k == 'gps':
                locReportOutside_res = quecIot.locReportOutside(v)
                if not locReportOutside_res:
                    res = False
                    break
            elif k == 'non_gps':
                locReportInside_res = quecIot.locReportInside(v)
                if not locReportInside_res:
                    res = False
                    break
            else:
                v = {}
                continue

            res = self.get_post_res()
            if res:
                v = {}
            else:
                res = False
                break

        self.rm_empty_data(data)
        return res

    def eventCB(self, data):
        log.info("event: %s" % str(data))
        event = data[0]
        errcode = data[1]
        if len(data) > 2:
            data = data[2]

        if event == 1:
            if errcode == 10200:
                log.info('Device authentication succeeded.')
            elif errcode == 10422:
                log.error('Device has been authenticated (connect failed).')
        elif event == 2:
            if errcode == 10200:
                log.info('Access succeeded.')
            if errcode == 10450:
                log.error('Device internal error (connect failed).')
        elif event == 3:
            if errcode == 10200:
                log.info('Subscription succeeded.')
                self.ota_request()
                if data != (3, 10200):
                    ota_info = data.decode()
                    file_info = ota_info.split(',')
                    log.info(
                        "OTA File Info: componentNo: %s, sourceVersion: %s, targetVersion: %s, "
                        "batteryLimit: %s, minSignalIntensity: %s, minSignalIntensity: %s" % tuple(file_info)
                    )
            if errcode == 10300:
                log.info('Subscription failed.')
        elif event == 4:
            if errcode == 10200:
                log.info('Data sending succeeded.')
                self.post_result_wait_queue.put(True)
            elif errcode == 10210:
                log.info('Object model data sending succeeded.')
                self.post_result_wait_queue.put(True)
            elif errcode == 10220:
                log.info('Location data sending succeeded.')
                self.post_result_wait_queue.put(True)
            elif errcode == 10300:
                log.info('Data sending failed.')
                self.post_result_wait_queue.put(False)
            elif errcode == 10310:
                log.error('Object model data sending failed.')
                self.post_result_wait_queue.put(False)
            elif errcode == 10320:
                log.error('Location data sending failed.')
                self.post_result_wait_queue.put(False)
        elif event == 5:
            if errcode == 10200:
                log.info('Recving raw data.')
                log.info(data)
                # TODO: To Check data format.
                self.downlink_queue.put(('raw_data', data))
            if errcode == 10210:
                log.info('Recving object model data.')
                dl_data = [(dict(object_model)[k][0], v.decode() if isinstance(v, bytes) else v) for k, v in data.items() if 'w' in dict(object_model)[k][1]]
                self.downlink_queue.put(('object_model', dl_data))
            elif errcode == 10211:
                log.info('Recving object model query command.')
                # TODO: Check pkgId for other uses.
                # log.info('pkgId: %s' % data[0])
                object_model_ids = data[1]
                object_model_val = [dict(object_model)[i][0] for i in object_model_ids if dict(object_model).get(i) is not None and 'r' in dict(object_model)[i][1]]
                self.downlink_queue.put(('query', object_model_val))
        elif event == 6:
            if errcode == 10200:
                log.info('Logout succeeded.')
        elif event == 7:
            if errcode == 10700:
                log.info('New OTA plain.')
                file_info = eval(data)
                log.info(
                    "OTA File Info: componentNo: %s, sourceVersion: %s, targetVersion: %s, "
                    "batteryLimit: %s, minSignalIntensity: %s, useSpace: %s" % tuple(file_info)
                )
                self.downlink_queue.put(('object_model', [('ota_status', (file_info[0], 1, file_info[2]))]))
                self.downlink_queue.put(('ota_plain', file_info))
            elif errcode == 10701:
                log.info('The module starts to download.')
                if data != (7, 10701):
                    file_info = eval(data)
                    log.debug('file_info: %s' % str(file_info))
                    self.sota_download_info(int(file_info[1]), file_info[2], int(file_info[3]))
                self.downlink_queue.put(('object_model', [('ota_status', (None, 2, None))]))
            elif errcode == 10702:
                log.info('Package download.')
                self.downlink_queue.put(('object_model', [('ota_status', (None, 2, None))]))
            elif errcode == 10703:
                log.info('Package download complete.')
                if data != (7, 10703):
                    file_info = eval(data)
                    log.info("OTA File Info: componentNo: %s, length: %s, md5: %s, crc: %s" % tuple(file_info))
                    self.sota_download_success(int(file_info[2]), int(file_info[3]))
                self.downlink_queue.put(('object_model', [('ota_status', (None, 2, None))]))
            elif errcode == 10704:
                log.info('Package updating.')
                self.downlink_queue.put(('object_model', [('ota_status', (None, 2, None))]))
            elif errcode == 10705:
                log.info('Firmware update complete.')
                self.downlink_queue.put(('object_model', [('ota_status', (None, 3, None))]))
            elif errcode == 10706:
                log.info('Failed to update firmware.')
                self.downlink_queue.put(('object_model', [('ota_status', (None, 4, None))]))
            elif errcode == 10707:
                log.info('Received confirmation broadcast.')

    def ota_request(self):
        quecIot.otaRequest(0)

    def ota_action(self, val=1):
        quecIot.otaAction(val)

    def sota_download_info(self, size, md5_value, crc):
        self.file_size = size
        self.crc_value = crc
        self.download_size = 0
        self.md5_value = md5_value
        self.update_mode = SotaDownloadUpgrade()

    def sota_download_success(self, start, down_loaded_size):
        self.need_download_size = down_loaded_size
        self.start_addr = start
        self.read_sota_file()

    def read_sota_file(self):
        while self.need_download_size != 0:
            readsize = 4096
            if (readsize > self.need_download_size):
                readsize = self.need_download_size
            updateFile = quecIot.mcuFWDataRead(self.start_addr, readsize)
            self.update_mode.write_update_data(updateFile)
            log.debug("Download File Size: %s" % readsize)
            self.need_download_size -= readsize
            self.start_addr += readsize
            self.download_size += readsize
            if (self.download_size == self.file_size):
                log.debug("File Download Success, Update Start.")
                quecIot.otaAction(3)
                file_update_res = self.update_mode.file_update(self.md5_value)
                if file_update_res:
                    self.update_mode.sota_set_flag()
                    log.debug("File Update Success, Power Restart.")
                else:
                    log.debug("File Update Failed.")
                self.downlink_queue.put(('object_model', [('power_restart', 1)]))
            else:
                quecIot.otaAction(2)
